package com.cruel_wandering.wifi_geo_predictor;

import java.util.LinkedList;

import com.cruel_wandering.wifi_geo_predictor.location.MyLocationManager;
import com.cruel_wandering.wifi_geo_predictor.location.MyLocationProvider;
import com.cruel_wandering.wifi_geo_predictor.util.Pair;
import com.cruel_wandering.wifi_geo_predictor.wifi_simulation.MyWifiInfo;
import com.cruel_wandering.wifi_geo_predictor.wifi_simulation.MyWifiManager;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

public class Oracle extends Service implements
  SharedPreferences.OnSharedPreferenceChangeListener
{
	private static final String TAG = "Oracle";

	private static final int HISTORY_SIZE = 5;
	// delta for 1 meter in degrees
	private static final double delta = 0.000008993;
	// area where we want to find existed wifi zones (in meters)
	private static final int area = 1000;  
	//from this distance we don't want to turn off wifi(in meters)
	private static final int nearAreaMeter = 10000;
	//this is a minimal distance to the closest wifi zone that 
	//we want to look on it(in meters)
	private static final int minimalDistance = 1000;
	
	// mul. coefficient for convertion from km/h to m/msec
	private static final double coeff = 0.000277778;
	private static final double highSpeed = 300 * coeff;
	private static final double midSpeed = 60 * coeff;
	private static final double lowSpeed = 30 * coeff;
	
	// default delay time in the case when speed > 300 km/h
	private static final int halfOfHour = 1800000;
	// default delay time in the case when speed > 60 km/h and < 300 km/h
	private static final int minute = 60000;
	// default delay time in the case when speed > 30 km/h and < 60 km/h
	private static final int halfOfMinute = 30000;

	static private LinkedList<LocationListener> notifyList =
		new LinkedList<LocationListener>();
	
	private boolean wifiPredicted = false;
	private DataBase db;
	private Context ctx;
	static private LinkedList<Location> history  = new LinkedList<Location>();
	private MyLocationListener listener;

	/**
	 * Returns the best available location provider accordingly to current
	 * preferences.
	 * See {@link MyLocationManager.getBestProvider} for more details. 
	 * @param lm
	 * @return best available provider
	 */
	private String getBestProvider(MyLocationManager lm)
	{
		SharedPreferences pm = PreferenceManager.getDefaultSharedPreferences(ctx);
		//boolean gpsEnabled = pm.getBoolean(Preferences.MAY_USE_GPS, true);
		boolean cellIdEnabled = pm.getBoolean(Preferences.MAY_USE_CELLID, true);
		Criteria c = new Criteria();

		c.setCostAllowed(cellIdEnabled);
		c.setAltitudeRequired(false);
		c.setBearingRequired(false);
		c.setPowerRequirement(Criteria.NO_REQUIREMENT);
		c.setSpeedRequired(false);
		c.setAccuracy(Criteria.ACCURACY_COARSE);

		return lm.getBestProvider(c, true);
	}

	private class MyLocationListener implements LocationListener {
		
		public void onLocationChanged(Location loc) {
			MyLocationManager lm = MyLocationManager.getInstance(ctx);
			log("Oracle: onLocationChanged.");

			locationPolled(loc);
			int pollDelay = checkWifiNearZones(loc);
			log("Next poll delay is " + pollDelay);
			
			if (wifiPredicted()) {
				log("Oracle predicted wifi availability in this location ");
				//lm.removeUpdates(this);
				return;
			}

			/* XXX: Both GPS and CellID providers assume that after every
			 * location update the listeners are reregistered.
			 * Thus, if you modify modify the next two lines, make sure
			 * location polling still works. 
			 */
			lm.removeUpdates(this);
			lm.requestLocationUpdates(getBestProvider(lm), pollDelay, 0, this);
		}

		public void onProviderDisabled(String provider) {
			log("Provider disabled");
		}

		public void onProviderEnabled(String provider) {
			log("Provider enabled");
		}

		public void onStatusChanged(String provider, int status, Bundle extras) {
			log("Status changed to " + status);
		}
	}

	/**
	 * Start Oracle. Start tracking the user and predicting, predicting and even
	 * more predicting.
	 */
	private void start()
	{
		log("Oracle started");
		MyLocationManager lm = MyLocationManager.getInstance(ctx);
		int minTimeChange = Integer.parseInt((PreferenceManager
				.getDefaultSharedPreferences(ctx)).getString(
				Preferences.MIN_POLLING_INTERVAL, "0"));
		String provider = getBestProvider(lm);

		log("Location poll task is being run. " +
			"Enabling location provider: <" + provider + ">");
		lm.requestLocationUpdates(provider, minTimeChange, 0, listener);
	}

	/**
	 * Cares the new polled location. Add to the history and make other useful
	 * calculations.
	 * 
	 * @param loc
	 *            a new polled location
	 */
	public void locationPolled(Location loc) {
		int latitude = (int) (loc.getLatitude() * 1E6);
		int longitude = (int) (loc.getLongitude() * 1E6);

		log("Location chaged to (lng, lat) = (" + longitude + ", " + latitude
				+ ")");
		if (history.size() >= HISTORY_SIZE)
			history.removeFirst();
		history.addLast(loc);

		if (notifyList.size() > 0)
		{
			for (LocationListener listener : notifyList)
				listener.onLocationChanged(loc);
		}
		updateDB(loc);
	}

	/**
	 * Make prediction about WiFi connectivity
	 * 
	 * @return the prediction, true if WiFi is available, false otherwise
	 */
	private boolean wifiPredicted() {
		return wifiPredicted;
	}

	/**
	 * Update the DB with a just polled location. I hope max will eventually
	 * comment this function, or one day I will get it by myself :)
	 * 
	 * @param loc
	 *            a polled location
	 */
	private void updateDB(Location loc) {
		Cursor cursor = null;

		try {
			MyWifiManager wifiMan = MyWifiManager.getManager();
			DBEntry entry = null;
			if (wifiMan.isWifiEnabled()) {
				MyWifiInfo wifiInfa = wifiMan.getConnectionInfo();

				if (wifiInfa.getBSSID() != null) {
					// there exists wifi cover and we want to add
					// information about that to DB
					entry = new DBEntry(wifiInfa, loc);
				}
			}
			if(entry != null){
				db.open();
				cursor = db.getTitle(entry.getBSSID());
				if(cursor == null || !cursor.moveToFirst()){
					Toast.makeText(this.getApplicationContext(),
							"WiFi zone is unknown. We need to add it to Data Base",
							Toast.LENGTH_LONG).show();
					db.insertTitle(entry);
				}else{
					updateTitle(cursor, loc);
					Toast.makeText(this.getApplicationContext(),
							"WiFi zone is known.",Toast.LENGTH_LONG).show();
				}

			}
//			else{
//				Toast.makeText(this.getApplicationContext(),
//						"There are no WiFi coverage in this point.",
//						Toast.LENGTH_LONG).show();
//			}
		} catch (Exception e) {
			Log.e(TAG,"Update.DB "+ e.toString() + ". Msg: " + e.getMessage());
		} finally {
			Log.e(TAG,"Oracle. Update.DB . Finalizer.");
			if (cursor != null && !cursor.isClosed())
				cursor.close();
			if (db.getDatabase() != null && db.getDatabase().isOpen())
				db.getDatabase().close();
		}
	}

	/**
	 * function check all near known wifi zones and take the closest one (if
	 * exist in the minimalDistination range)
	 * 
	 * @param loc
	 *            a polled location
	 */
	private int checkWifiNearZones(Location loc)
	{
		double lat = loc.getLatitude();
		double lon = loc.getLongitude();
		Pair<Float, Integer> resPair = computeParams(lat, lon);
		float minDist = (Float) resPair.first;
		int radius = (Integer)resPair.second;
		MyWifiManager wifiMan = MyWifiManager.getManager();
		if (minDist > radius + Oracle.nearAreaMeter) {
			if (wifiMan.isWifiEnabled()) {
				wifiMan.setWifiEnabled(false);
				Toast.makeText(this.getApplicationContext(),
					"WiFi connection are disabled.",Toast.LENGTH_LONG).show();
			}
			wifiPredicted = false;
			return computeNewParamaters(minDist - radius);
		}
		if (minDist > radius) {
			MyWifiInfo WifiInfa = wifiMan.getConnectionInfo();
			if (wifiMan.isWifiEnabled() && WifiInfa.getBSSID() != null) {
				// TODO we are near to the wifi covered zone. We need to compute new
				//try {		// radius if we come out form this zone but still have wifi coverage.
				// updateRadius(loc, WifiInfa.getBSSID() );
				wifiPredicted = true;
			}
			if (wifiMan.isWifiEnabled() && WifiInfa.getBSSID() == null) {
				Toast.makeText(this.getApplicationContext(),
						"WiFi connection are lost.", Toast.LENGTH_LONG)
						.show();
				wifiPredicted = false;
			}
			return computeNewParamaters(minDist - radius);
		} else {
			if (!wifiMan.isWifiEnabled()) {
				wifiMan.setWifiEnabled(true);
				Toast.makeText(this.getApplicationContext(),
								"WiFi connection are avaliable.",
								Toast.LENGTH_LONG).show();
			}
			wifiPredicted = true;
			return computeNewParamaters(radius / 2);
		}
	}

	/**
	 * Calculate a time till next location polling based on location history
	 * 
	 * @return the time in ms
	 */
	@SuppressWarnings("finally")
	private int computeNewParamaters(float minDist)
	{
		SharedPreferences prefs = PreferenceManager
			.getDefaultSharedPreferences(ctx);
		int min_interval = Integer.parseInt(prefs.getString(
			Preferences.MIN_POLLING_INTERVAL, "0"));
		int max_interval = Integer.parseInt(prefs.getString(
			Preferences.MAX_POLLING_INTERVAL, "0"));
		int calculatedTime = -1;

		try {
			int histSize = history.size();
			if (histSize < 2)
				return min_interval;

			Location curLoc = history.getLast();
			Location prevLoc = history.get(histSize - 2);
			float[] distanceBetween = new float[1];
			Location.distanceBetween(curLoc.getLatitude(), curLoc
					.getLongitude(), prevLoc.getLatitude(), prevLoc
					.getLongitude(), distanceBetween);
			long timeBetween = (curLoc.getTime() - prevLoc.getTime());
			double speed = distanceBetween[0] / timeBetween;

			calculatedTime = (int) (minDist / speed);
			if ( speed > Oracle.highSpeed)
				calculatedTime = (Oracle.halfOfHour > calculatedTime ) ? Oracle.halfOfHour : calculatedTime ;
			else if ( speed > Oracle.midSpeed)
				calculatedTime = (Oracle.minute > calculatedTime ) ? Oracle.minute : calculatedTime ;
			else if ( speed > Oracle.lowSpeed)
				calculatedTime = (Oracle.halfOfMinute > calculatedTime ) ? Oracle.halfOfMinute : calculatedTime ;
		} catch (Exception e) {
			Log.e(TAG, e.toString());
			calculatedTime = -1;
		} finally {
			Log.e(TAG, "calculated time = " + calculatedTime + "; (max, min) = (" +
					max_interval + ", " + min_interval + ")");

			return (calculatedTime < min_interval) ? min_interval :
				((calculatedTime > max_interval) ? max_interval : calculatedTime);
		}
	}

	/**
	 * Calculate a new radius for specific access point based on the current
	 * location
	 * 
	 * @param loc
	 *            current location
	 * @param bssid
	 *            current access point
	 */
	@SuppressWarnings("unused")
	// updateRadius()
	private void updateRadius(Location loc, String bssid) {
		/* XXX: to do in the future */
	}

	/**
	 * Send a message to the log. It is added more for debug purposes than for
	 * log ones.
	 * 
	 * @param msg
	 *            a log message
	 */
	private void log(String msg) {
		Logger.getInstance(ctx).log(TAG, msg);
	}
	

	@Override
	public void onCreate() {
		super.onCreate();
		this.ctx = this;
		this.listener = new MyLocationListener();
		this.db = DataBase.getInstance(this);

		log("onCreate is called");
		log("got db = " + ((Object) db));
	}

	@Override
	public void onStart(Intent intent, int startId)
	{
		super.onStart(intent, startId);

		/* Enable/disable providers according to Preferences */
		MyLocationManager lm = MyLocationManager.getInstance(ctx);
	    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);

	    lm.getProvider(MyLocationManager.GPS_PROVIDER).setEnabled(
			prefs.getBoolean(Preferences.MAY_USE_GPS, false));
	    lm.getProvider(MyLocationManager.NETWORK_PROVIDER).setEnabled(
			prefs.getBoolean(Preferences.MAY_USE_CELLID, false));

	    prefs.registerOnSharedPreferenceChangeListener(this);

		start();
		showNotification("WifiGeoPredictor", "Predictor is started");
	}

	@Override
	public void onDestroy() {
		MyLocationManager lm = MyLocationManager.getInstance(this);

		log("onDestroy is called");
		lm.removeUpdates(listener);
		hideNotification();
		super.onDestroy();
	}

	@Override
	public IBinder onBind(Intent intent) {
		log("Oracle.onBind(" + intent.getDataString() + ") is called");
		return null;
	}

	/**
	 * Show a notification while this service is running.
	 */
	private void showNotification(String title, String text) {
		@SuppressWarnings("unused")
		NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
		Notification notification = new Notification(0, text, System
				.currentTimeMillis());
		PendingIntent i = PendingIntent.getActivity(this, 0, new Intent(this,
				MainScreen.class), 0);

		log("show notification: \"" + title + "\", \"" + text + "\"");
		// Set the info for the views that show in the notification panel.
		notification.setLatestEventInfo(this, title, text, i);
		// We use a layout id because it is a unique number. We use it later to
		// cancel.
		// nm.notify(1, notification);
	}

	private void hideNotification() {
		@SuppressWarnings("unused")
		NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

		log("Hide notification");
		// nm.cancel(1);
	}

	/* Disable/enable GPS/CellID providers according to the preferences */
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
    String key)
  {
		MyLocationProvider p = null;
		String prefKey = null;

		if (Preferences.MAY_USE_GPS.equals(key))
		{
			p = MyLocationManager.getInstance(ctx)
			  .getProvider(MyLocationManager.GPS_PROVIDER);
			prefKey = Preferences.MAY_USE_GPS;
		}
		else if (Preferences.MAY_USE_CELLID.equals(key))
		{
			p = MyLocationManager.getInstance(ctx)
			  .getProvider(MyLocationManager.NETWORK_PROVIDER);
			prefKey = Preferences.MAY_USE_CELLID;
		}

		if (p == null)
			return;

	  boolean val = sharedPreferences.getBoolean(prefKey, false);
	  log(prefKey + " is set to " + val);
	  p.setEnabled(val);
  }

	static public void requestLocationUpdates(LocationListener listener)
  {
		Log.e(TAG, "listener was added to Oracle.");
		notifyList.add(listener); 
  }

	static public void removeUpdates(LocationListener listener)
	{
		Log.e(TAG, "listener was remouved from Oracle.");
		notifyList.remove(listener);
	}
	
	
	
	
	private void computeParams(double params[] , double lat, double lon){
		double delta = (Oracle.area * Oracle.delta);
		if (lat >= 0.0) {
			if (lon >= 0.0) {
				params[0] = lat - delta;
				params[1] = lat + delta;
				params[2] = lon - delta;
				params[3] = lon + delta;
			} else {
				params[0] = lat - delta;
				params[1] = lat + delta;
				params[2] = lon + delta;
				params[3] = lon - delta;
			}

		} else {
			if (lon >= 0.0) {
				params[0] = lat + delta;
				params[1] = lat - delta;
				params[2] = lon - delta;
				params[3] = lon + delta;
			} else {
				params[0] = lat + delta;
				params[1] = lat - delta;
				params[2] = lon + delta;
				params[3] = lon - delta;
			}
		}
	}
	
	
	

	private Pair<Float, Integer> computeParams(double lat, double lon){
		Cursor cursor;
		float[] results = new float[1];
		double params[] = new double[4];
		int radius = 0;
		float minDist = (float)minimalDistance;
		computeParams(params , lat, lon);
		db.open();
		cursor = db.getAllTitlesInRange(params[0], params[1], params[2],params[3]);
		if (cursor != null && cursor.moveToFirst())
		{
			do {
				double lat2 = cursor.getDouble(3);
				double lon2 = cursor.getDouble(4);
				Location.distanceBetween(lat, lon, lat2, lon2, results);
				if (results[0] < minDist) {
					minDist = results[0];
					radius = cursor.getInt(2);
				}
			} while (cursor.moveToNext());
			if (!cursor.isClosed())
			    cursor.close();
		}
		if (db.getDatabase().isOpen())
			db.getDatabase().close();
		Log.e(TAG, "minDist = " + minDist + "; radius = (" +
				radius + ")");
		return new Pair<Float, Integer>(minDist,radius);
	}

	
	/**
	 * Compute new parameters for title.
	 * May be too "heavy" for computation because of for each new 
	 * point we works hard with data base. 
	 */
	private void updateTitle(Cursor cursor, Location loc){
		double newPointLat = loc.getLatitude();
		double newPointLng = loc.getLongitude();
		double centralPointLat = cursor.getDouble(3);
		double centralPointLng = cursor.getDouble(4);
		int radius = cursor.getInt(2);
		double sumPointLat = 0.0;
		double sumPointLng = 0.0;
		double tmpPointLat = 0.0;
		double tmpPointLng = 0.0;
		int j = 0;
		int minIndex = 0;
		float[] result = new float[1];
		float minDistPoint = 0;
		float maxDistPoint = 0;
		for(int i = 6; i < 17 ;){
			try{
				tmpPointLat = cursor.getDouble(i++);
				tmpPointLng = cursor.getDouble(i++);
				if(tmpPointLat == newPointLat && tmpPointLng == newPointLng)
					return;
				j++;
				Location.distanceBetween(tmpPointLat, tmpPointLng, centralPointLat, centralPointLng, result);
				if(i == 8){
					minDistPoint = result[0];
					minIndex = j;
				}else{
					if(result[0] < minDistPoint ){
						minDistPoint = result[0];
						minIndex = j;
					}
				}
				if(result[0] > maxDistPoint)
					maxDistPoint = result[0];
			}catch(Exception e){
				break;
			}
			i++;
			sumPointLat += tmpPointLat;
			sumPointLng += tmpPointLng;
		}
		if(j <= 4){
			db.updateTitleAddPointValue(cursor.getString(1) ,newPointLat, newPointLng , j);
		}
		else{
			Location.distanceBetween(newPointLat, newPointLng, centralPointLat, centralPointLng, result);
			if(result[0] > minDistPoint){
				db.updateTitleAddPointValue(cursor.getString(1) ,newPointLat,newPointLng , minIndex);
			}	
		}
		sumPointLat = sumPointLat / j;
		sumPointLng = sumPointLng / j;
		db.updateTitleAddPointValue(cursor.getString(1) ,sumPointLat,sumPointLng , 0);
		if(maxDistPoint > radius)
			db.updateTitleRadius(cursor.getString(1), (int)maxDistPoint);
	}

	
	static public Location getLastLocation(){
		if( !history.isEmpty() ){
			return history.getLast();
		}
		return null;
	}
	
}
